﻿namespace Edge.Utilities.Linq
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Security.Cryptography;
    using System.Text;
    using System.Web;
    using System.Web.Caching;

    /// <summary>
    /// Allows for the caching of linq query results in memory.
    /// </summary>
    public static class QueryResultCache
    {
        /// <summary>
        /// Returns the result of the query; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// The cache entry has a one minute sliding expiration with normal priority.
        /// </summary>
        /// <typeparam name="T">Type of list.</typeparam>
        /// <param name="query">Query to cache.</param>
        /// <returns>If the list exists in a cache, returns it.  Otherwise, performs the query.</returns>
        public static IList<T> FromCache<T>(this IQueryable<T> query)
        {
            return query.FromCache(CacheItemPriority.Normal, TimeSpan.FromMinutes(1));
        }

        /// <summary>
        /// Returns the result of the query; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of list.</typeparam>
        /// <param name="query">Query to cache.</param>
        /// <param name="priority">Cache item priority.</param>
        /// <param name="slidingExpiration">Sliding expiration.</param>
        /// <returns>If the list exists in a cache, returns it.  Otherwise, performs the query.</returns>
        public static IList<T> FromCache<T>(this IQueryable<T> query, CacheItemPriority priority, TimeSpan slidingExpiration)
        {
            // use the string representation of the query for the cache key
            // (locally evaluate as much of the query as possible to ensure
            // member access expression values appear in the string)
            string key = Evaluator.PartialEval(query.Expression).ToString();

            // the key is potentially very long, so use an md5 fingerprint
            // (fine if the query result data isn't critically sensitive)
            key = key.ToMd5Fingerprint();

            var result = HttpRuntime.Cache.Get(key) as List<T>;

            if (result == null)
            {
                // materialize the query
                result = query.ToList();

                HttpRuntime.Cache.Insert(
                    key,
                    result,
                    null, // no cache dependency
                    Cache.NoAbsoluteExpiration,
                    slidingExpiration,
                    priority,
                    null); // no removal notification
            }

            return result;
        }

        /// <summary>
        /// Creates an MD5 fingerprint of the string.
        /// </summary>
        /// <param name="s">String to convert.</param>
        /// <returns>Results of the conversion.</returns>
        private static string ToMd5Fingerprint(this string s)
        {
            var bytes = Encoding.Unicode.GetBytes(s.ToCharArray());

            var hash = new MD5CryptoServiceProvider().ComputeHash(bytes);

            // concat the hash bytes into one long string
            return hash.Aggregate(new StringBuilder(32), (sb, b) => sb.Append(b.ToString("X2"))).ToString();
        }
    }
}
